package tr3dtris.game.models.pieces;

import java.awt.Point;
import tr3dtris.game.models.Block;
import tr3dtris.game.models.CellArray;
import tr3dtris.utils.MathUtils;

/**
 * Abstract class used as base to build all the game pieces.
 * @author Luciano Mammino <loige@hotmail.com>
 * @author Andrea Mangano <kapitan0@hotmail.it>
 * @version 0.1
 * @since 0.1
 */
public abstract class AbstractPiece implements Cloneable
{
    /**
     * X coordinate of piece on a particular <CellArray> used as grid.
     */
    protected int x;

    /**
     * Y coordinate of a piece on a particular <CellArray> used as grid.
     */
    protected int y;

    /**
     * Reference to the grid where the piece is contained
     */
    private CellArray parentGrid;

    /**
     * Flag used to make the piece visible or invisible
     */
    private boolean visible;


    /**
     * Type id of the piece. Every piece should have a different type id.
     * This should be usefful for the display to draw every different piece
     * using different methods (textures, colors, etc.)
     */
    protected int typeId;

    /**
     * Center point of the piece (used for calculating rotations).
     * Automatically calcolated using the #cells.getWidth() and
     * #cells.getHeight()
     */
    private Point center;

    /**
     * Local copy of #cells.getWidth()
     */
    protected int width;

    /**
     * Local copy of #cells.setWidth()
     */
    protected int height;

    /**
     * CellArray that contains all the blocks of the piece
     */
    protected CellArray cells;

    /**
     * The id type of the blocks contained in the AbstractPiece
     */
    private int blockType;

    /**
     * Rotation mask used to check blocks collision while rotating clockwise
     */
    protected boolean[][] cwRotationMask;

    /**
     * Rotation mask used to check blocks collision while rotating  counter
     * clockwise
     */
    protected boolean[][] ccwRotationMask;

    /**
     * Create the blocks on the #cells giving them the default type
     * @return
     */
    protected CellArray createBlocks()
    {
        return this.createBlocks(Block.DEFAULT_TYPE);
    }

    /**
     * Creates the blocks on the #cells giving them a given type
     * @param type
     * @return
     */
    protected abstract CellArray createBlocks(int type);

    /**
     * Creates the clockwise rotation mask
     * @return
     */
    protected abstract boolean[][] createCwRotationMask();

    /**
     * Creates the counter clockwise rotation mask
     * @return
     */
    protected abstract boolean[][] createCcwRotationMask();

    /**
     * Gets the type id of the piece
     * @return
     */
    public abstract int getTypeId();

    /**
     * Check if the piece could be rotated
     * @return
     */
    public abstract boolean isRotable();

    /**
     * Clone function. creates a new instance of the <AbstractPiece> with the
     * same features.
     */
    @Override
    public abstract AbstractPiece clone();

    /**
     * Constructor. Creates a new <AbstractPiece> using given coordinates and
     * a given blocktype
     * @param x X coordinates
     * @param y Y coordinates
     * @param blockType the type to assign to every block of the piece
     */
    public AbstractPiece(int x, int y, CellArray parentGrid, int blockType)
    {
        this.x = x;
        this.y = y;
        this.parentGrid = parentGrid;
        this.typeId = this.getTypeId();
        this.cells = this.createBlocks(blockType);
        this.cwRotationMask = this.createCwRotationMask();
        this.ccwRotationMask = this.createCcwRotationMask();
        this.width = this.cells.getWidth();
        this.height = this.cells.getHeight();
        this.center = new Point(this.getWidth()/2, this.getHeight()/2);
    }

    /**
     * Constructor. Creates a new <AbstractPiece> cloned by a given
     * <AbstractPiece>.
     * @param pieceToCopy The <AbstractPiece> to copy from
     */
    public AbstractPiece(AbstractPiece pieceToCopy)
    {
        this(   pieceToCopy.getX(),
                pieceToCopy.getY(),
                pieceToCopy.getParentGrid(),
                pieceToCopy.getBlockType()   );
        this.cells = (CellArray)pieceToCopy.getCells().clone();
        this.ccwRotationMask = pieceToCopy.getCcwRotationMask();
        this.cwRotationMask = pieceToCopy.getCwRotationMask();
    }

    /**
     * @return the _x
     */
    public int getX()
    {
        return x;
    }

    /**
     * @param _x the _x to set
     */
    public void setX(int x)
    {
        this.x = x;
    }

    /**
     * @return the _y
     */
    public int getY()
    {
        return y;
    }

    /**
     * @param _y the _y to set
     */
    public void setY(int y)
    {
        this.y = y;
    }

    /**
     * @return the center
     */
    public Point getCenter()
    {
        return center;
    }

    /**
     * @return the width
     */
    public int getWidth()
    {
        return width;
    }

    /**
     * @return the height
     */
    public int getHeight()
    {
        return height;
    }

    /**
     * @return the blockType
     */
    public int getBlockType() {
        return blockType;
    }

    /**
     * @return the cells
     */
    public CellArray getCells()
    {
        return cells;
    }

    /**
     * @return the cwRotationMask
     */
    public boolean[][] getCwRotationMask()
    {
        return cwRotationMask;
    }

    /**
     * @return the ccwRotationMask
     */
    public boolean[][] getCcwRotationMask()
    {
        return ccwRotationMask;
    }

    /**
     * @return the parentGrid
     */
    public CellArray getParentGrid() {
        return parentGrid;
    }

    /**
     * @param parentGrid the parentGrid to set
     */
    public void setParentGrid(CellArray parentGrid) {
        this.parentGrid = parentGrid;
    }

    /**
     * @return the visible
     */
    public boolean isVisible() {
        return visible;
    }

    /**
     * @param visible the visible to set
     */
    public void setVisible(boolean visible) {
        this.visible = visible;
    }

    /**
     * Stores the piece into the parentGrid at the current coordinates
     */
    public void storeIntoParentGrid() throws PieceOutOfGridException
    {
        try
        {
            this.parentGrid.storePiece(this, x, y);
        }
        catch(IndexOutOfBoundsException e)
        {
            throw new PieceOutOfGridException(this, this.getParentGrid(), x, y);
        }
    }

    /**
     * Check wheter the piece can be moved of a cell to the right in the grid
     * @return
     */
    public boolean canMoveRight()
    {
        return (!this.collides(1, 0));
    }

    /**
     * Check wheter the piece can be moved of a cell to the left in the grid
     * @return
     */
    public boolean canMoveLeft()
    {
        return (!this.collides(-1, 0));
    }

    /**
     * Check wheter the piece can be moved of a cell down in the grid
     * @return
     */
    public boolean canMoveDown()
    {
        return (!this.collides(0, 1));
    }

    /**
     * Check wheter the piece can be moved of a cell up in the grid
     * @return
     */
    public boolean canMoveUp()
    {
        return (!this.collides(0, -1));
    }

    /**
     * Check wheter the piece can be moved rotated clockwise in the grid
     * @return
     */
    public boolean canRotateCw()
    {
        return this.canRotate(this.cwRotationMask);
    }

    /**
     * Check wheter the piece can be rotated counter clockwise in the grid
     * @return
     */
    public boolean canRotateCcw()
    {
        return this.canRotate(this.ccwRotationMask);
    }

    /**
     * Check if the piece can be rotated using a given rotation mask
     * @param mask rotation mask
     * @return
     */
    private boolean canRotate(boolean[][] mask)
    {
        for(int _y=0; _y < this.getHeight(); _y++)
        {
            for(int _x=0; _x < this.getWidth(); _x++)
            {
                if(mask[_x][_y])
                {
                    if(!this.cellIsInsideGrid(_x, _y))
                        return false;

                    Point glob = this.localToGlobalCoordinates(_x, _y);
                    if(this.parentGrid.hasBlockAt(glob.x, glob.y))
                        return false;
                }
            }
        }
        return true;
    }

    /**
     * Rotate the piece clockwise (if the piece could be rotated)
     */
    public void rotateCw()
    {
        if(this.canRotateCw())
            this.rotate(90);
    }

    /**
     * Rotate the piece counter clockwise (if the piece could be rotated)
     */
    public void rotateCcw()
    {
        if(this.canRotateCcw())
            this.rotate(-90);
    }

    /**
     * Rotate the piece in a giving degree if the piece could be rotated
     * @param degree
     */
    private void rotate(int degree)
    {
        if(this.isRotable())
        {
            double theta = MathUtils.degree2rad(degree);
            int newW = this.getHeight();
            int newH = this.getWidth();
            CellArray tempCells = new CellArray(newW, newH);
            boolean[][] tempCwMask = new boolean[newW][newH];
            boolean[][] tempCcwMask = new boolean[newW][newH];

            for(int _x = 0; _x < this.getWidth(); _x++)
                for(int _y = 0; _y < this.getHeight(); _y++)
                {

                    Point r = MathUtils.rotatePoint(new Point(_x,_y),this.getCenter(),theta);

                    if(this.getCells().hasBlockAt(_x, _y))
                    {
                        if(r.x >= 0 && r.x < newW &&
                           r.y >= 0 && r.y < newH)
                        {
                            tempCells.setCellAt(r.x, r.y, this.getCells().getCellAt(_x, _y));
                        }
                    }

                    tempCwMask[r.x][r.y] = this.getCwRotationMask()[_x][_y];
                    tempCcwMask[r.x][r.y] = this.getCcwRotationMask()[_x][_y];
                }

            this.cells = tempCells;
            this.cwRotationMask = tempCwMask;
            this.ccwRotationMask = tempCcwMask;
        }
    }

    /**
     * Automatically moves the piece down until it collides with another block
     * or with the grid
     */
    public void drop()
    {
        while(this.canMoveDown())
            this.moveDown();
    }

    /**
     * Move the piece of one cell to the right (if can be moved)
     */
    public void moveRight()
    {
        if(this.canMoveRight())
            this.x++;
    }

    /**
     * Move the piece of one cell to the left (if can be moved)
     */
    public void moveLeft()
    {
        if(this.canMoveLeft())
            this.x--;
    }

    /**
     * Move the piece of one cell down (if can be moved)
     */
    public void moveDown()
    {
        if(this.canMoveDown())
            this.y++;
    }

    /**
     * Move the piece of one cell up (if can be moved)
     */
    public void moveUp()
    {
        if(this.canMoveUp())
           this.y--;
    }

    /**
     * Check if the piece collides with grid blocks or with grid borders
     * @return
     */
    private boolean collides()
    {
        for(int _y=0; _y < this.getHeight(); _y++)
        {
            for (int _x=0; _x < this.getWidth(); _x++)
            {
                if(this.cells.hasBlockAt(_x, _y))
                {
                    if( !this.cellIsInsideGrid(_x, _y))
                        return true;

                    Point glob = this.localToGlobalCoordinates(_x, _y);

                    if(this.parentGrid.hasBlockAt(glob.x, glob.y))
                        return true;
                }
            }
        }

        return false;
    }

    /**
     * Checks if the piece collides when its position is displaced.
     * Note that displacement could be also a negative value
     * @param xDisplacement the relative x displacement
     * @param yDisplacement the relative y displacement
     * @return
     */
    private boolean collides(int xDisplacement, int yDisplacement)
    {
        AbstractPiece checker = this.clone();
        checker.setX(checker.x + xDisplacement);
        checker.setY(checker.y + yDisplacement);
        return checker.collides();
    }

    /**
     * Checks wheter cell coordinates are inside the #parentGrid
     * @param _x
     * @param _y
     * @return
     */
    private boolean cellIsInsideGrid(int _x, int _y)
    {
        Point glob = this.localToGlobalCoordinates(_x, _y);
        if( this.parentGrid.isInside(glob.x, glob.y, false) )
            return true;

        return false;
    }

    /**
     * Converts local coordinate of a piece cell to global coordinates referred
     * to the #parentGrid
     * @param _x
     * @param _y
     * @return
     */
    private Point localToGlobalCoordinates(int _x, int _y)
    {
        return new Point(this.x + _x, this.y + _y);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString()
    {
        String ret = "";
        for(int _y=0; _y < this.cells.getHeight(); _y++)
        {
            for( int _x=0; _x < this.cells.getWidth(); _x++)
            {
                if(this.cells.hasBlockAt(_x, _y))
                    ret += "1";
                else
                    ret += "0";
            }
            ret += "\n";
        }
        return ret;
    }


}